/**
Copyright Christiaan Ottow 2009
chris@6core.net

See README for info
 
 This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package visim;

import java.util.*;
import java.io.*;

/**
 * This class is responsible for starting the simulation.
 * It creates threads that simulate individual clients.
 */
public class Visim
{
	/** Version **/
	public static final String VERSION = "0.1";
	
	/** Max. number of threads */
	public static final int THREAD_MAX = 2500;
		
	/** test name **/
	private String tag = "test";
	
	/** location of the webapp to be tested */
	private String host = "localhost";
	
	/** port of the httpd on the server */
	private int port = 80;
	
	/** test duration in msecs **/
	private long runtime = 1000;
	
	/** wait time between request bursts **/
	private long wait;
	
	/** number of clients **/
	private int num_clients = 10;
	
	/** local address, source address for requests */
	private String localAddress = null;
	
	/** number of different local addresses to use */
	private int numAddresses = 1;
	
	/** URLs to request */
	private Vector<String> getRequests;
	
	private TreeMap<String,String> postRequests;
		
	/** criteria for success */
	private TreeMap<Float,Integer> requirements;
	
	/** Visitor connections */
	private Vector<Client> clients;
	
	/** timing results */
	private Vector<Integer> results;
	
	/** response logging */
	private boolean doResponseLogging = false;
	
	/**
	 * creates needed objects
	 */
	public Visim( String configFile )
	{
		int i;
		clients = new Vector<Client>();
		this.results = new Vector<Integer>();
		
		ConfigReader cr = new ConfigReader( this );
		if( !cr.readFile( configFile ) )
		{
			System.exit(1);
		}
		
		System.out.println( "Running test for " + ( runtime / 1000 ) + " seconds on: ");
		System.out.println( "================================");
		
		int c = 0;
		if( !(new File("logs").exists()) )
		{
			new File("logs").mkdir();
		}
		
		String logdir = "logs/" + this.tag;
		while( new File(logdir).exists() )
		{
			c++;
			logdir = "logs/" + this.tag + "-" + c;
		}
		
		new File(logdir).mkdir();
		
		System.out.println("Using logdir "+logdir);
		
		int counter = 0, base=0;
		String[] parts = null;
		if( localAddress != null )
		{
			parts = localAddress.split("\\.");
			counter = base = Integer.parseInt( parts[3] );
		}

		String addr = localAddress;
		for( i=1; i<=num_clients; i++ )
		{
			if( localAddress != null )
			{
				addr = parts[0]+"."+parts[1]+"."+parts[2]+"."+counter;
				if( counter - base == ( this.numAddresses-1)  )
				{
					counter = base;
				} else {
					counter++;
				}
				
			}
			
			Client cli = new Client( i, getRequests, postRequests );
			cli.setHost( host );
			cli.setPort( port );
			cli.setLocalAddr(addr);
			cli.setSleepTime(this.wait);
			cli.setLogDir( logdir );
			cli.setLogfile( logdir+"/thread-"+i+".log" );
			cli.setResponseLogging( doResponseLogging );
			clients.add( cli );
		}
	}
	
	public void runSimulation( )
	{
		// start the timer
		SimulationDurationTimer t = new SimulationDurationTimer( this, runtime );
		t.start();
		
		// start up the visitor threads
		Iterator<Client> it = clients.iterator();
		Client cl;
		
		System.out.print( "Starting client threads... " );
		while( it.hasNext() )
		{
			cl = it.next();
			cl.start();
		}
		System.out.println("done");
		System.out.println("Test is running, be patient");
	}
	
	public void stopSimulation()
	{
		Iterator<Client> it = clients.iterator();
		Client cl;
		
		System.out.print( "Killing visitor threads... " );
		
		while( it.hasNext() )
		{
			cl = it.next();
			cl.stopNext();
			this.results.addAll( cl.getResults() );
		}
		
		// now, wait for all thread to stop
		it = clients.iterator();
		while( it.hasNext() )
		{
			cl = it.next();
			try {
				cl.join();
			} catch( Exception e ) {
				System.err.println("Don't know what happened");
			}
		}
		
		System.out.println("done");
		
	}
	
	public void parseResults()
	{
		PrintWriter fw =  null;

		// open output file
		File outfile = null;
		int fc = 0;
		String fname = this.tag+"-all.csv";
		while( new File(fname).exists() )
		{
			fc++;
			fname = this.tag+"-all-"+fc+".csv";
		}
		
		try {
			outfile = new File( fname );
			fw = new PrintWriter( outfile );
		} catch( Exception e ) {
			System.err.println( "Caught exception" );
		}
		
		Iterator<Integer> i1 = this.results.iterator();
		
		long sum=0;
		double avg;
		long count=0;
		int val;
		
		while( i1.hasNext() )
		{
			val = i1.next().intValue();
			fw.print( val );
			if( i1.hasNext() )
			{
				fw.print(",");
			}
			sum += val;
			count++;
		}
		
		try {
			fw.close();
		} catch( Exception e ) {
			System.err.println( "Caught exception" );
		}
		
		avg = sum / count;
		System.out.println( "Number of requests \t"+count );
		System.out.println( "Average delay \t\t"+avg+"ms");
		
	}
	
	/** Test if the given criteria were met */
	public void checkRequirements()
	{
		Collections.sort( results );
		int errors = 0;
		// replace values of -1 with value above highest requirement since these represent errors
		
		int max_limit = requirements.get( requirements.lastKey() );
		if( results.get(0) == -1 )
		{
			while( results.get(errors) == -1 )
			{
				errors++;
			}
			
			int i;
			for( i=0; i<errors; i++ )
			{
				results.remove(0);
				results.add( max_limit+1 );
			}
		}
		
		if( errors > 0 )
		{
			System.out.println("!! "+errors+" requests dit not return code 200");
		}
		
		System.out.println("==========================================================");
		for( Map.Entry<Float,Integer> crit : requirements.entrySet() )
		{
			float perc = crit.getKey();
			int limit = crit.getValue();
			
			int num = (int)( (results.size() / 100.0) * perc );
			boolean met = results.get( num - 1 ) <= limit;
			System.out.print("  Requirement "+perc+" <= "+limit+"\t "+num+" requests: \t"+met);
			if( !met )
			{
				System.out.println("\t"+perc+"% <= "+results.get(num-1) );
			} else {
				System.out.println();
			}
		}
		System.out.println("==========================================================");
	}
	
	/**
	 * gets argument, number of threads, and starts simulation
	 */
	public static void main( String[] args )
	{
		String config = "benchmark.conf";
		
		if( args.length > 0 )
		{
			config = args[0];
		}
		
		Visim vs = new Visim( config);
		vs.runSimulation();
	}

	public String getTag() {
		return tag;
	}

	public void setTag(String tag) {
		this.tag = tag;
	}

	public String getHost() {
		return host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public int getPort() {
		return port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public long getRuntime() {
		return runtime;
	}

	public void setRuntime(long runtime) {
		this.runtime = runtime;
	}

	public long getWait() {
		return wait;
	}

	public void setWait(long wait) {
		this.wait = wait;
	}
	
	public String getLocalAddress() {
		return this.localAddress;
	}
	
	public void setLocalAddress( String ad ) {
		this.localAddress = ad;
	}

	public int getNumClients() {
		return this.num_clients;
	}
	
	public void setNumClients( int num ) {
		this.num_clients = num;
	}
	
	public void setNumAddresses( int num ) {
		this.numAddresses = num;
	}
	
	public Vector<String> getGetRequests() {
		return getRequests;
	}

	public void setGetRequests(Vector<String> paths) {
		this.getRequests = paths;
	}

	public void setPostRequests( TreeMap<String,String> paths )
	{
		this.postRequests = paths;
	}
	
	public void setResponseLogging( boolean log ) {
		this.doResponseLogging = log;
	}
	
	public boolean getResponseLogging() {
		return this.doResponseLogging;
	}
	
	public TreeMap<String,String> getPostRequests() {
		return this.postRequests;
	}
	
	public TreeMap<Float, Integer> getRequirements() {
		return requirements;
	}

	public void setRequirements(TreeMap<Float, Integer> requirements) {
		this.requirements = requirements;
	}

	public Vector<Integer> getResults() {
		return results;
	}

	public void setResults(Vector<Integer> results) {
		this.results = results;
	}
}
